/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import "vs/css!./media/openeditors"
import * as nls from "vs/nls"
import { RunOnceScheduler } from "vs/base/common/async"
import {
  IAction,
  ActionRunner,
  WorkbenchActionExecutedEvent,
  WorkbenchActionExecutedClassification,
} from "vs/base/common/actions"
import * as dom from "vs/base/browser/dom"
import { IContextMenuService } from "vs/platform/contextview/browser/contextView"
import {
  IInstantiationService,
  ServicesAccessor,
} from "vs/platform/instantiation/common/instantiation"
import {
  IEditorGroupsService,
  IEditorGroup,
  GroupsOrder,
  GroupOrientation,
} from "vs/workbench/services/editor/common/editorGroupsService"
import {
  IConfigurationService,
  IConfigurationChangeEvent,
} from "vs/platform/configuration/common/configuration"
import { IKeybindingService } from "vs/platform/keybinding/common/keybinding"
import {
  Verbosity,
  EditorResourceAccessor,
  SideBySideEditor,
  IEditorIdentifier,
  GroupModelChangeKind,
  preventEditorClose,
  EditorCloseMethod,
} from "vs/workbench/common/editor"
import { EditorInput } from "vs/workbench/common/editor/editorInput"
import {
  SaveAllInGroupAction,
  CloseGroupAction,
} from "vs/workbench/contrib/files/browser/fileActions"
import {
  OpenEditorsFocusedContext,
  ExplorerFocusedContext,
  IFilesConfiguration,
  OpenEditor,
} from "vs/workbench/contrib/files/common/files"
import {
  CloseAllEditorsAction,
  CloseEditorAction,
  UnpinEditorAction,
} from "vs/workbench/browser/parts/editor/editorActions"
import {
  IContextKeyService,
  IContextKey,
  ContextKeyExpr,
} from "vs/platform/contextkey/common/contextkey"
import { IThemeService } from "vs/platform/theme/common/themeService"
import {
  asCssVariable,
  badgeBackground,
  badgeForeground,
  contrastBorder,
} from "vs/platform/theme/common/colorRegistry"
import { WorkbenchList } from "vs/platform/list/browser/listService"
import {
  IListVirtualDelegate,
  IListRenderer,
  IListContextMenuEvent,
  IListDragAndDrop,
  IListDragOverReaction,
  ListDragOverEffectPosition,
  ListDragOverEffectType,
} from "vs/base/browser/ui/list/list"
import { ResourceLabels, IResourceLabel } from "vs/workbench/browser/labels"
import { ActionBar } from "vs/base/browser/ui/actionbar/actionbar"
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry"
import { DisposableMap, IDisposable, dispose } from "vs/base/common/lifecycle"
import {
  MenuId,
  Action2,
  registerAction2,
  MenuRegistry,
} from "vs/platform/actions/common/actions"
import {
  OpenEditorsDirtyEditorContext,
  OpenEditorsGroupContext,
  OpenEditorsReadonlyEditorContext,
  SAVE_ALL_LABEL,
  SAVE_ALL_COMMAND_ID,
  NEW_UNTITLED_FILE_COMMAND_ID,
} from "vs/workbench/contrib/files/browser/fileConstants"
import {
  ResourceContextKey,
  MultipleEditorGroupsContext,
} from "vs/workbench/common/contextkeys"
import {
  CodeDataTransfers,
  containsDragType,
} from "vs/platform/dnd/browser/dnd"
import {
  ResourcesDropHandler,
  fillEditorsDragData,
} from "vs/workbench/browser/dnd"
import { ViewPane } from "vs/workbench/browser/parts/views/viewPane"
import { IViewletViewOptions } from "vs/workbench/browser/parts/views/viewsViewlet"
import { IDragAndDropData, DataTransfers } from "vs/base/browser/dnd"
import { memoize } from "vs/base/common/decorators"
import {
  ElementsDragAndDropData,
  ListViewTargetSector,
  NativeDragAndDropData,
} from "vs/base/browser/ui/list/listView"
import { IWorkingCopyService } from "vs/workbench/services/workingCopy/common/workingCopyService"
import {
  IWorkingCopy,
  WorkingCopyCapabilities,
} from "vs/workbench/services/workingCopy/common/workingCopy"
import { IFilesConfigurationService } from "vs/workbench/services/filesConfiguration/common/filesConfigurationService"
import { IViewDescriptorService } from "vs/workbench/common/views"
import { IOpenerService } from "vs/platform/opener/common/opener"
import { Orientation } from "vs/base/browser/ui/splitview/splitview"
import { IListAccessibilityProvider } from "vs/base/browser/ui/list/listWidget"
import { compareFileNamesDefault } from "vs/base/common/comparers"
import { Codicon } from "vs/base/common/codicons"
import { KeyCode, KeyMod } from "vs/base/common/keyCodes"
import { KeybindingWeight } from "vs/platform/keybinding/common/keybindingsRegistry"
import { ICommandService } from "vs/platform/commands/common/commands"
import { Schemas } from "vs/base/common/network"
import { extUriIgnorePathCase } from "vs/base/common/resources"
import { ILocalizedString } from "vs/platform/action/common/action"
import { mainWindow } from "vs/base/browser/window"
import { EditorGroupView } from "vs/workbench/browser/parts/editor/editorGroupView"
import { IHoverService } from "vs/platform/hover/browser/hover"

const $ = dom.$

export class OpenEditorsView extends ViewPane {
  private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9
  private static readonly DEFAULT_MIN_VISIBLE_OPEN_EDITORS = 0
  static readonly ID = "workbench.explorer.openEditorsView"
  static readonly NAME: ILocalizedString = nls.localize2(
    { key: "openEditors", comment: ["Open is an adjective"] },
    "Open Editors"
  )

  private dirtyCountElement!: HTMLElement
  private listRefreshScheduler: RunOnceScheduler | undefined
  private structuralRefreshDelay: number
  private dnd: OpenEditorsDragAndDrop | undefined
  private list: WorkbenchList<OpenEditor | IEditorGroup> | undefined
  private listLabels: ResourceLabels | undefined
  private needsRefresh = false
  private elements: (OpenEditor | IEditorGroup)[] = []
  private sortOrder: "editorOrder" | "alphabetical" | "fullPath"
  private resourceContext!: ResourceContextKey
  private groupFocusedContext!: IContextKey<boolean>
  private dirtyEditorFocusedContext!: IContextKey<boolean>
  private readonlyEditorFocusedContext!: IContextKey<boolean>
  private blockFocusActiveEditorTracking = false

  constructor(
    options: IViewletViewOptions,
    @IInstantiationService instantiationService: IInstantiationService,
    @IViewDescriptorService viewDescriptorService: IViewDescriptorService,
    @IContextMenuService contextMenuService: IContextMenuService,
    @IEditorGroupsService
    private readonly editorGroupService: IEditorGroupsService,
    @IConfigurationService configurationService: IConfigurationService,
    @IKeybindingService keybindingService: IKeybindingService,
    @IContextKeyService contextKeyService: IContextKeyService,
    @IThemeService themeService: IThemeService,
    @ITelemetryService telemetryService: ITelemetryService,
    @IHoverService hoverService: IHoverService,
    @IWorkingCopyService
    private readonly workingCopyService: IWorkingCopyService,
    @IFilesConfigurationService
    private readonly filesConfigurationService: IFilesConfigurationService,
    @IOpenerService openerService: IOpenerService
  ) {
    super(
      options,
      keybindingService,
      contextMenuService,
      configurationService,
      contextKeyService,
      viewDescriptorService,
      instantiationService,
      openerService,
      themeService,
      telemetryService,
      hoverService
    )

    this.structuralRefreshDelay = 0
    this.sortOrder = configurationService.getValue(
      "explorer.openEditors.sortOrder"
    )

    this.registerUpdateEvents()

    // Also handle configuration updates
    this._register(
      this.configurationService.onDidChangeConfiguration((e) =>
        this.onConfigurationChange(e)
      )
    )

    // Handle dirty counter
    this._register(
      this.workingCopyService.onDidChangeDirty((workingCopy) =>
        this.updateDirtyIndicator(workingCopy)
      )
    )
  }

  private registerUpdateEvents(): void {
    const updateWholeList = () => {
      if (!this.isBodyVisible() || !this.list) {
        this.needsRefresh = true
        return
      }

      this.listRefreshScheduler?.schedule(this.structuralRefreshDelay)
    }

    const groupDisposables = this._register(new DisposableMap<number>())
    const addGroupListener = (group: IEditorGroup) => {
      const groupModelChangeListener = group.onDidModelChange((e) => {
        if (this.listRefreshScheduler?.isScheduled()) {
          return
        }
        if (!this.isBodyVisible() || !this.list) {
          this.needsRefresh = true
          return
        }

        const index = this.getIndex(group, e.editor)
        switch (e.kind) {
          case GroupModelChangeKind.EDITOR_ACTIVE:
            this.focusActiveEditor()
            break
          case GroupModelChangeKind.GROUP_INDEX:
          case GroupModelChangeKind.GROUP_LABEL:
            if (index >= 0) {
              this.list.splice(index, 1, [group])
            }
            break
          case GroupModelChangeKind.EDITOR_DIRTY:
          case GroupModelChangeKind.EDITOR_STICKY:
          case GroupModelChangeKind.EDITOR_CAPABILITIES:
          case GroupModelChangeKind.EDITOR_PIN:
          case GroupModelChangeKind.EDITOR_LABEL:
            this.list.splice(index, 1, [new OpenEditor(e.editor!, group)])
            this.focusActiveEditor()
            break
          case GroupModelChangeKind.EDITOR_OPEN:
          case GroupModelChangeKind.EDITOR_MOVE:
          case GroupModelChangeKind.EDITOR_CLOSE:
            updateWholeList()
            break
        }
      })
      groupDisposables.set(group.id, groupModelChangeListener)
    }

    this.editorGroupService.groups.forEach((g) => addGroupListener(g))
    this._register(
      this.editorGroupService.onDidAddGroup((group) => {
        addGroupListener(group)
        updateWholeList()
      })
    )
    this._register(
      this.editorGroupService.onDidMoveGroup(() => updateWholeList())
    )
    this._register(
      this.editorGroupService.onDidChangeActiveGroup(() =>
        this.focusActiveEditor()
      )
    )
    this._register(
      this.editorGroupService.onDidRemoveGroup((group) => {
        groupDisposables.deleteAndDispose(group.id)
        updateWholeList()
      })
    )
  }

  protected override renderHeaderTitle(container: HTMLElement): void {
    super.renderHeaderTitle(container, this.title)

    const count = dom.append(
      container,
      $(".open-editors-dirty-count-container")
    )
    this.dirtyCountElement = dom.append(
      count,
      $(".dirty-count.monaco-count-badge.long")
    )

    this.dirtyCountElement.style.backgroundColor =
      asCssVariable(badgeBackground)
    this.dirtyCountElement.style.color = asCssVariable(badgeForeground)
    this.dirtyCountElement.style.border = `1px solid ${asCssVariable(
      contrastBorder
    )}`

    this.updateDirtyIndicator()
  }

  protected override renderBody(container: HTMLElement): void {
    super.renderBody(container)

    container.classList.add("open-editors")
    container.classList.add("show-file-icons")

    const delegate = new OpenEditorsDelegate()

    if (this.list) {
      this.list.dispose()
    }
    if (this.listLabels) {
      this.listLabels.clear()
    }

    this.dnd = new OpenEditorsDragAndDrop(
      this.sortOrder,
      this.instantiationService,
      this.editorGroupService
    )

    this.listLabels = this.instantiationService.createInstance(ResourceLabels, {
      onDidChangeVisibility: this.onDidChangeBodyVisibility,
    })
    this.list = this.instantiationService.createInstance(
      WorkbenchList,
      "OpenEditors",
      container,
      delegate,
      [
        new EditorGroupRenderer(
          this.keybindingService,
          this.instantiationService
        ),
        new OpenEditorRenderer(
          this.listLabels,
          this.instantiationService,
          this.keybindingService,
          this.configurationService
        ),
      ],
      {
        identityProvider: {
          getId: (element: OpenEditor | IEditorGroup) =>
            element instanceof OpenEditor
              ? element.getId()
              : element.id.toString(),
        },
        dnd: this.dnd,
        overrideStyles: this.getLocationBasedColors().listOverrideStyles,
        accessibilityProvider: new OpenEditorsAccessibilityProvider(),
      }
    ) as WorkbenchList<OpenEditor | IEditorGroup>
    this._register(this.list)
    this._register(this.listLabels)

    // Register the refresh scheduler
    let labelChangeListeners: IDisposable[] = []
    this.listRefreshScheduler = this._register(
      new RunOnceScheduler(() => {
        // No need to refresh the list if it's not rendered
        if (!this.list) {
          return
        }
        labelChangeListeners = dispose(labelChangeListeners)
        const previousLength = this.list.length
        const elements = this.getElements()
        this.list.splice(0, this.list.length, elements)
        this.focusActiveEditor()
        if (previousLength !== this.list.length) {
          this.updateSize()
        }
        this.needsRefresh = false

        if (
          this.sortOrder === "alphabetical" ||
          this.sortOrder === "fullPath"
        ) {
          // We need to resort the list if the editor label changed
          elements.forEach((e) => {
            if (e instanceof OpenEditor) {
              labelChangeListeners.push(
                e.editor.onDidChangeLabel(() =>
                  this.listRefreshScheduler?.schedule()
                )
              )
            }
          })
        }
      }, this.structuralRefreshDelay)
    )

    this.updateSize()

    // Bind context keys
    OpenEditorsFocusedContext.bindTo(this.list.contextKeyService)
    ExplorerFocusedContext.bindTo(this.list.contextKeyService)

    this.resourceContext =
      this.instantiationService.createInstance(ResourceContextKey)
    this._register(this.resourceContext)
    this.groupFocusedContext = OpenEditorsGroupContext.bindTo(
      this.contextKeyService
    )
    this.dirtyEditorFocusedContext = OpenEditorsDirtyEditorContext.bindTo(
      this.contextKeyService
    )
    this.readonlyEditorFocusedContext = OpenEditorsReadonlyEditorContext.bindTo(
      this.contextKeyService
    )

    this._register(this.list.onContextMenu((e) => this.onListContextMenu(e)))
    this.list.onDidChangeFocus((e) => {
      this.resourceContext.reset()
      this.groupFocusedContext.reset()
      this.dirtyEditorFocusedContext.reset()
      this.readonlyEditorFocusedContext.reset()
      const element = e.elements.length ? e.elements[0] : undefined
      if (element instanceof OpenEditor) {
        const resource = element.getResource()
        this.dirtyEditorFocusedContext.set(
          element.editor.isDirty() && !element.editor.isSaving()
        )
        this.readonlyEditorFocusedContext.set(!!element.editor.isReadonly())
        this.resourceContext.set(resource ?? null)
      } else if (!!element) {
        this.groupFocusedContext.set(true)
      }
    })

    // Open when selecting via keyboard
    this._register(
      this.list.onMouseMiddleClick((e) => {
        if (e && e.element instanceof OpenEditor) {
          if (
            preventEditorClose(
              e.element.group,
              e.element.editor,
              EditorCloseMethod.MOUSE,
              this.editorGroupService.partOptions
            )
          ) {
            return
          }

          e.element.group.closeEditor(e.element.editor, { preserveFocus: true })
        }
      })
    )
    this._register(
      this.list.onDidOpen((e) => {
        const element = e.element
        if (!element) {
          return
        } else if (element instanceof OpenEditor) {
          if (dom.isMouseEvent(e.browserEvent) && e.browserEvent.button === 1) {
            return // middle click already handled above: closes the editor
          }

          this.withActiveEditorFocusTrackingDisabled(() => {
            this.openEditor(element, {
              preserveFocus: e.editorOptions.preserveFocus,
              pinned: e.editorOptions.pinned,
              sideBySide: e.sideBySide,
            })
          })
        } else {
          this.withActiveEditorFocusTrackingDisabled(() => {
            this.editorGroupService.activateGroup(element)
            if (!e.editorOptions.preserveFocus) {
              element.focus()
            }
          })
        }
      })
    )

    this.listRefreshScheduler.schedule(0)

    this._register(
      this.onDidChangeBodyVisibility((visible) => {
        if (visible && this.needsRefresh) {
          this.listRefreshScheduler?.schedule(0)
        }
      })
    )

    const containerModel = this.viewDescriptorService.getViewContainerModel(
      this.viewDescriptorService.getViewContainerByViewId(this.id)!
    )!
    this._register(
      containerModel.onDidChangeAllViewDescriptors(() => {
        this.updateSize()
      })
    )
  }

  override focus(): void {
    super.focus()

    this.list?.domFocus()
  }

  protected override layoutBody(height: number, width: number): void {
    super.layoutBody(height, width)
    this.list?.layout(height, width)
  }

  private get showGroups(): boolean {
    return this.editorGroupService.groups.length > 1
  }

  private getElements(): Array<IEditorGroup | OpenEditor> {
    this.elements = []
    this.editorGroupService
      .getGroups(GroupsOrder.GRID_APPEARANCE)
      .forEach((g) => {
        if (this.showGroups) {
          this.elements.push(g)
        }
        let editors = g.editors.map((ei) => new OpenEditor(ei, g))
        if (this.sortOrder === "alphabetical") {
          editors = editors.sort((first, second) =>
            compareFileNamesDefault(
              first.editor.getName(),
              second.editor.getName()
            )
          )
        } else if (this.sortOrder === "fullPath") {
          editors = editors.sort((first, second) => {
            const firstResource = first.editor.resource
            const secondResource = second.editor.resource
            //put 'system' editors before everything
            if (firstResource === undefined && secondResource === undefined) {
              return compareFileNamesDefault(
                first.editor.getName(),
                second.editor.getName()
              )
            } else if (firstResource === undefined) {
              return -1
            } else if (secondResource === undefined) {
              return 1
            } else {
              const firstScheme = firstResource.scheme
              const secondScheme = secondResource.scheme
              //put non-file editors before files
              if (
                firstScheme !== Schemas.file &&
                secondScheme !== Schemas.file
              ) {
                return extUriIgnorePathCase.compare(
                  firstResource,
                  secondResource
                )
              } else if (firstScheme !== Schemas.file) {
                return -1
              } else if (secondScheme !== Schemas.file) {
                return 1
              } else {
                return extUriIgnorePathCase.compare(
                  firstResource,
                  secondResource
                )
              }
            }
          })
        }
        this.elements.push(...editors)
      })

    return this.elements
  }

  private getIndex(
    group: IEditorGroup,
    editor: EditorInput | undefined | null
  ): number {
    if (!editor) {
      return this.elements.findIndex(
        (e) => !(e instanceof OpenEditor) && e.id === group.id
      )
    }

    return this.elements.findIndex(
      (e) =>
        e instanceof OpenEditor &&
        e.editor === editor &&
        e.group.id === group.id
    )
  }

  private openEditor(
    element: OpenEditor,
    options: { preserveFocus?: boolean; pinned?: boolean; sideBySide?: boolean }
  ): void {
    if (element) {
      this.telemetryService.publicLog2<
        WorkbenchActionExecutedEvent,
        WorkbenchActionExecutedClassification
      >("workbenchActionExecuted", {
        id: "workbench.files.openFile",
        from: "openEditors",
      })

      const preserveActivateGroup = options.sideBySide && options.preserveFocus // needed for https://github.com/microsoft/vscode/issues/42399
      if (!preserveActivateGroup) {
        this.editorGroupService.activateGroup(element.group) // needed for https://github.com/microsoft/vscode/issues/6672
      }
      const targetGroup = options.sideBySide
        ? this.editorGroupService.sideGroup
        : element.group
      targetGroup.openEditor(element.editor, options)
    }
  }

  private onListContextMenu(
    e: IListContextMenuEvent<OpenEditor | IEditorGroup>
  ): void {
    if (!e.element) {
      return
    }

    const element = e.element

    this.contextMenuService.showContextMenu({
      menuId: MenuId.OpenEditorsContext,
      menuActionOptions: {
        shouldForwardArgs: true,
        arg:
          element instanceof OpenEditor
            ? EditorResourceAccessor.getOriginalUri(element.editor)
            : {},
      },
      contextKeyService: this.list?.contextKeyService,
      getAnchor: () => e.anchor,
      getActionsContext: () =>
        element instanceof OpenEditor
          ? {
              groupId: element.groupId,
              editorIndex: element.group.getIndexOfEditor(element.editor),
            }
          : { groupId: element.id },
    })
  }

  private withActiveEditorFocusTrackingDisabled(fn: () => void): void {
    this.blockFocusActiveEditorTracking = true
    try {
      fn()
    } finally {
      this.blockFocusActiveEditorTracking = false
    }
  }

  private focusActiveEditor(): void {
    if (!this.list || this.blockFocusActiveEditorTracking) {
      return
    }

    if (this.list.length && this.editorGroupService.activeGroup) {
      const index = this.getIndex(
        this.editorGroupService.activeGroup,
        this.editorGroupService.activeGroup.activeEditor
      )
      if (index >= 0) {
        try {
          this.list.setFocus([index])
          this.list.setSelection([index])
          this.list.reveal(index)
        } catch (e) {
          // noop list updated in the meantime
        }
        return
      }
    }

    this.list.setFocus([])
    this.list.setSelection([])
  }

  private onConfigurationChange(event: IConfigurationChangeEvent): void {
    if (event.affectsConfiguration("explorer.openEditors")) {
      this.updateSize()
    }
    // Trigger a 'repaint' when decoration settings change or the sort order changed
    if (
      event.affectsConfiguration("explorer.decorations") ||
      event.affectsConfiguration("explorer.openEditors.sortOrder")
    ) {
      this.sortOrder = this.configurationService.getValue(
        "explorer.openEditors.sortOrder"
      )
      if (this.dnd) {
        this.dnd.sortOrder = this.sortOrder
      }
      this.listRefreshScheduler?.schedule()
    }
  }

  private updateSize(): void {
    // Adjust expanded body size
    this.minimumBodySize =
      this.orientation === Orientation.VERTICAL
        ? this.getMinExpandedBodySize()
        : 170
    this.maximumBodySize =
      this.orientation === Orientation.VERTICAL
        ? this.getMaxExpandedBodySize()
        : Number.POSITIVE_INFINITY
  }

  private updateDirtyIndicator(workingCopy?: IWorkingCopy): void {
    if (workingCopy) {
      const gotDirty = workingCopy.isDirty()
      if (
        gotDirty &&
        !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) &&
        this.filesConfigurationService.hasShortAutoSaveDelay(
          workingCopy.resource
        )
      ) {
        return // do not indicate dirty of working copies that are auto saved after short delay
      }
    }

    const dirty = this.workingCopyService.dirtyCount
    if (dirty === 0) {
      this.dirtyCountElement.classList.add("hidden")
    } else {
      this.dirtyCountElement.textContent = nls.localize(
        "dirtyCounter",
        "{0} unsaved",
        dirty
      )
      this.dirtyCountElement.classList.remove("hidden")
    }
  }

  private get elementCount(): number {
    return this.editorGroupService.groups
      .map((g) => g.count)
      .reduce(
        (first, second) => first + second,
        this.showGroups ? this.editorGroupService.groups.length : 0
      )
  }

  private getMaxExpandedBodySize(): number {
    let minVisibleOpenEditors = this.configurationService.getValue<number>(
      "explorer.openEditors.minVisible"
    )
    // If it's not a number setting it to 0 will result in dynamic resizing.
    if (typeof minVisibleOpenEditors !== "number") {
      minVisibleOpenEditors = OpenEditorsView.DEFAULT_MIN_VISIBLE_OPEN_EDITORS
    }
    const containerModel = this.viewDescriptorService.getViewContainerModel(
      this.viewDescriptorService.getViewContainerByViewId(this.id)!
    )!
    if (containerModel.visibleViewDescriptors.length <= 1) {
      return Number.POSITIVE_INFINITY
    }

    return (
      Math.max(this.elementCount, minVisibleOpenEditors) *
      OpenEditorsDelegate.ITEM_HEIGHT
    )
  }

  private getMinExpandedBodySize(): number {
    let visibleOpenEditors = this.configurationService.getValue<number>(
      "explorer.openEditors.visible"
    )
    if (typeof visibleOpenEditors !== "number") {
      visibleOpenEditors = OpenEditorsView.DEFAULT_VISIBLE_OPEN_EDITORS
    }

    return this.computeMinExpandedBodySize(visibleOpenEditors)
  }

  private computeMinExpandedBodySize(
    visibleOpenEditors = OpenEditorsView.DEFAULT_VISIBLE_OPEN_EDITORS
  ): number {
    const itemsToShow = Math.min(
      Math.max(visibleOpenEditors, 1),
      this.elementCount
    )
    return itemsToShow * OpenEditorsDelegate.ITEM_HEIGHT
  }

  setStructuralRefreshDelay(delay: number): void {
    this.structuralRefreshDelay = delay
  }

  override getOptimalWidth(): number {
    if (!this.list) {
      return super.getOptimalWidth()
    }

    const parentNode = this.list.getHTMLElement()
    const childNodes: HTMLElement[] = [].slice.call(
      parentNode.querySelectorAll(".open-editor > a")
    )

    return dom.getLargestChildWidth(parentNode, childNodes)
  }
}

interface IOpenEditorTemplateData {
  container: HTMLElement
  root: IResourceLabel
  actionBar: ActionBar
  actionRunner: OpenEditorActionRunner
}

interface IEditorGroupTemplateData {
  root: HTMLElement
  name: HTMLSpanElement
  actionBar: ActionBar
  editorGroup: IEditorGroup
}

class OpenEditorActionRunner extends ActionRunner {
  public editor: OpenEditor | undefined

  override async run(action: IAction): Promise<void> {
    if (!this.editor) {
      return
    }

    return super.run(action, {
      groupId: this.editor.groupId,
      editorIndex: this.editor.group.getIndexOfEditor(this.editor.editor),
    })
  }
}

class OpenEditorsDelegate
  implements IListVirtualDelegate<OpenEditor | IEditorGroup>
{
  public static readonly ITEM_HEIGHT = 22

  getHeight(_element: OpenEditor | IEditorGroup): number {
    return OpenEditorsDelegate.ITEM_HEIGHT
  }

  getTemplateId(element: OpenEditor | IEditorGroup): string {
    if (element instanceof OpenEditor) {
      return OpenEditorRenderer.ID
    }

    return EditorGroupRenderer.ID
  }
}

class EditorGroupRenderer
  implements IListRenderer<IEditorGroup, IEditorGroupTemplateData>
{
  static readonly ID = "editorgroup"

  constructor(
    private keybindingService: IKeybindingService,
    private instantiationService: IInstantiationService
  ) {
    // noop
  }

  get templateId() {
    return EditorGroupRenderer.ID
  }

  renderTemplate(container: HTMLElement): IEditorGroupTemplateData {
    const editorGroupTemplate: IEditorGroupTemplateData = Object.create(null)
    editorGroupTemplate.root = dom.append(container, $(".editor-group"))
    editorGroupTemplate.name = dom.append(
      editorGroupTemplate.root,
      $("span.name")
    )
    editorGroupTemplate.actionBar = new ActionBar(container)

    const saveAllInGroupAction = this.instantiationService.createInstance(
      SaveAllInGroupAction,
      SaveAllInGroupAction.ID,
      SaveAllInGroupAction.LABEL
    )
    const saveAllInGroupKey = this.keybindingService.lookupKeybinding(
      saveAllInGroupAction.id
    )
    editorGroupTemplate.actionBar.push(saveAllInGroupAction, {
      icon: true,
      label: false,
      keybinding: saveAllInGroupKey ? saveAllInGroupKey.getLabel() : undefined,
    })

    const closeGroupAction = this.instantiationService.createInstance(
      CloseGroupAction,
      CloseGroupAction.ID,
      CloseGroupAction.LABEL
    )
    const closeGroupActionKey = this.keybindingService.lookupKeybinding(
      closeGroupAction.id
    )
    editorGroupTemplate.actionBar.push(closeGroupAction, {
      icon: true,
      label: false,
      keybinding: closeGroupActionKey
        ? closeGroupActionKey.getLabel()
        : undefined,
    })

    return editorGroupTemplate
  }

  renderElement(
    editorGroup: IEditorGroup,
    _index: number,
    templateData: IEditorGroupTemplateData
  ): void {
    templateData.editorGroup = editorGroup
    templateData.name.textContent = editorGroup.label
    templateData.actionBar.context = { groupId: editorGroup.id }
  }

  disposeTemplate(templateData: IEditorGroupTemplateData): void {
    templateData.actionBar.dispose()
  }
}

class OpenEditorRenderer
  implements IListRenderer<OpenEditor, IOpenEditorTemplateData>
{
  static readonly ID = "openeditor"

  private readonly closeEditorAction = this.instantiationService.createInstance(
    CloseEditorAction,
    CloseEditorAction.ID,
    CloseEditorAction.LABEL
  )
  private readonly unpinEditorAction = this.instantiationService.createInstance(
    UnpinEditorAction,
    UnpinEditorAction.ID,
    UnpinEditorAction.LABEL
  )

  constructor(
    private labels: ResourceLabels,
    private instantiationService: IInstantiationService,
    private keybindingService: IKeybindingService,
    private configurationService: IConfigurationService
  ) {
    // noop
  }

  get templateId() {
    return OpenEditorRenderer.ID
  }

  renderTemplate(container: HTMLElement): IOpenEditorTemplateData {
    const editorTemplate: IOpenEditorTemplateData = Object.create(null)
    editorTemplate.container = container
    editorTemplate.actionRunner = new OpenEditorActionRunner()
    editorTemplate.actionBar = new ActionBar(container, {
      actionRunner: editorTemplate.actionRunner,
    })
    editorTemplate.root = this.labels.create(container)

    return editorTemplate
  }

  renderElement(
    openedEditor: OpenEditor,
    _index: number,
    templateData: IOpenEditorTemplateData
  ): void {
    const editor = openedEditor.editor
    templateData.actionRunner.editor = openedEditor
    templateData.container.classList.toggle(
      "dirty",
      editor.isDirty() && !editor.isSaving()
    )
    templateData.container.classList.toggle("sticky", openedEditor.isSticky())
    templateData.root.setResource(
      {
        resource: EditorResourceAccessor.getOriginalUri(editor, {
          supportSideBySide: SideBySideEditor.BOTH,
        }),
        name: editor.getName(),
        description: editor.getDescription(Verbosity.MEDIUM),
      },
      {
        italic: openedEditor.isPreview(),
        extraClasses: ["open-editor"].concat(
          openedEditor.editor.getLabelExtraClasses()
        ),
        fileDecorations:
          this.configurationService.getValue<IFilesConfiguration>().explorer
            .decorations,
        title: editor.getTitle(Verbosity.LONG),
        icon: editor.getIcon(),
      }
    )
    const editorAction = openedEditor.isSticky()
      ? this.unpinEditorAction
      : this.closeEditorAction
    if (!templateData.actionBar.hasAction(editorAction)) {
      if (!templateData.actionBar.isEmpty()) {
        templateData.actionBar.clear()
      }
      templateData.actionBar.push(editorAction, {
        icon: true,
        label: false,
        keybinding: this.keybindingService
          .lookupKeybinding(editorAction.id)
          ?.getLabel(),
      })
    }
  }

  disposeTemplate(templateData: IOpenEditorTemplateData): void {
    templateData.actionBar.dispose()
    templateData.root.dispose()
    templateData.actionRunner.dispose()
  }
}

class OpenEditorsDragAndDrop
  implements IListDragAndDrop<OpenEditor | IEditorGroup>
{
  private _sortOrder: "editorOrder" | "alphabetical" | "fullPath"
  public set sortOrder(value: "editorOrder" | "alphabetical" | "fullPath") {
    this._sortOrder = value
  }

  constructor(
    sortOrder: "editorOrder" | "alphabetical" | "fullPath",
    private instantiationService: IInstantiationService,
    private editorGroupService: IEditorGroupsService
  ) {
    this._sortOrder = sortOrder
  }

  @memoize private get dropHandler(): ResourcesDropHandler {
    return this.instantiationService.createInstance(ResourcesDropHandler, {
      allowWorkspaceOpen: false,
    })
  }

  getDragURI(element: OpenEditor | IEditorGroup): string | null {
    if (element instanceof OpenEditor) {
      const resource = element.getResource()
      if (resource) {
        return resource.toString()
      }
    }
    return null
  }

  getDragLabel?(elements: (OpenEditor | IEditorGroup)[]): string {
    if (elements.length > 1) {
      return String(elements.length)
    }
    const element = elements[0]

    return element instanceof OpenEditor
      ? element.editor.getName()
      : element.label
  }

  onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
    const items = (data as ElementsDragAndDropData<OpenEditor | IEditorGroup>)
      .elements
    const editors: IEditorIdentifier[] = []
    if (items) {
      for (const item of items) {
        if (item instanceof OpenEditor) {
          editors.push(item)
        }
      }
    }

    if (editors.length) {
      // Apply some datatransfer types to allow for dragging the element outside of the application
      this.instantiationService.invokeFunction(
        fillEditorsDragData,
        editors,
        originalEvent
      )
    }
  }

  onDragOver(
    data: IDragAndDropData,
    _targetElement: OpenEditor | IEditorGroup,
    _targetIndex: number,
    targetSector: ListViewTargetSector | undefined,
    originalEvent: DragEvent
  ): boolean | IListDragOverReaction {
    if (data instanceof NativeDragAndDropData) {
      if (
        !containsDragType(
          originalEvent,
          DataTransfers.FILES,
          CodeDataTransfers.FILES
        )
      ) {
        return false
      }
    }

    if (this._sortOrder !== "editorOrder") {
      if (data instanceof ElementsDragAndDropData) {
        // No reordering supported when sorted
        return false
      } else {
        // Allow droping files to open them
        return {
          accept: true,
          effect: { type: ListDragOverEffectType.Move },
          feedback: [-1],
        }
      }
    }

    let dropEffectPosition: ListDragOverEffectPosition | undefined = undefined
    switch (targetSector) {
      case ListViewTargetSector.TOP:
      case ListViewTargetSector.CENTER_TOP:
        dropEffectPosition =
          _targetIndex === 0 && _targetElement instanceof EditorGroupView
            ? ListDragOverEffectPosition.After
            : ListDragOverEffectPosition.Before
        break
      case ListViewTargetSector.CENTER_BOTTOM:
      case ListViewTargetSector.BOTTOM:
        dropEffectPosition = ListDragOverEffectPosition.After
        break
    }

    return {
      accept: true,
      effect: {
        type: ListDragOverEffectType.Move,
        position: dropEffectPosition,
      },
      feedback: [_targetIndex],
    }
  }

  drop(
    data: IDragAndDropData,
    targetElement: OpenEditor | IEditorGroup | undefined,
    _targetIndex: number,
    targetSector: ListViewTargetSector | undefined,
    originalEvent: DragEvent
  ): void {
    let group =
      targetElement instanceof OpenEditor
        ? targetElement.group
        : targetElement ||
          this.editorGroupService.groups[this.editorGroupService.count - 1]
    let targetEditorIndex =
      targetElement instanceof OpenEditor
        ? targetElement.group.getIndexOfEditor(targetElement.editor)
        : 0

    switch (targetSector) {
      case ListViewTargetSector.TOP:
      case ListViewTargetSector.CENTER_TOP:
        if (targetElement instanceof EditorGroupView && group.index !== 0) {
          group = this.editorGroupService.groups[group.index - 1]
          targetEditorIndex = group.count
        }
        break
      case ListViewTargetSector.BOTTOM:
      case ListViewTargetSector.CENTER_BOTTOM:
        if (targetElement instanceof OpenEditor) {
          targetEditorIndex++
        }
        break
    }

    if (data instanceof ElementsDragAndDropData) {
      for (const oe of data.elements) {
        const sourceEditorIndex = oe.group.getIndexOfEditor(oe.editor)
        if (oe.group === group && sourceEditorIndex < targetEditorIndex) {
          targetEditorIndex--
        }
        oe.group.moveEditor(oe.editor, group, {
          index: targetEditorIndex,
          preserveFocus: true,
        })
        targetEditorIndex++
      }
      this.editorGroupService.activateGroup(group)
    } else {
      this.dropHandler.handleDrop(
        originalEvent,
        mainWindow,
        () => group.focus(),
        { index: targetEditorIndex }
      )
    }
  }

  dispose(): void {}
}

class OpenEditorsAccessibilityProvider
  implements IListAccessibilityProvider<OpenEditor | IEditorGroup>
{
  getWidgetAriaLabel(): string {
    return nls.localize("openEditors", "Open Editors")
  }

  getAriaLabel(element: OpenEditor | IEditorGroup): string | null {
    if (element instanceof OpenEditor) {
      return `${element.editor.getName()}, ${element.editor.getDescription()}`
    }

    return element.ariaLabel
  }
}

const toggleEditorGroupLayoutId = "workbench.action.toggleEditorGroupLayout"
registerAction2(
  class extends Action2 {
    constructor() {
      super({
        id: "workbench.action.toggleEditorGroupLayout",
        title: nls.localize2(
          "flipLayout",
          "Toggle Vertical/Horizontal Editor Layout"
        ),
        f1: true,
        keybinding: {
          primary: KeyMod.Shift | KeyMod.Alt | KeyCode.Digit0,
          mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Digit0 },
          weight: KeybindingWeight.WorkbenchContrib,
        },
        icon: Codicon.editorLayout,
        menu: {
          id: MenuId.ViewTitle,
          group: "navigation",
          when: ContextKeyExpr.and(
            ContextKeyExpr.equals("view", OpenEditorsView.ID),
            MultipleEditorGroupsContext
          ),
          order: 10,
        },
      })
    }

    async run(accessor: ServicesAccessor): Promise<void> {
      const editorGroupService = accessor.get(IEditorGroupsService)
      const newOrientation =
        editorGroupService.orientation === GroupOrientation.VERTICAL
          ? GroupOrientation.HORIZONTAL
          : GroupOrientation.VERTICAL
      editorGroupService.setGroupOrientation(newOrientation)
    }
  }
)

MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, {
  group: "5_flip",
  command: {
    id: toggleEditorGroupLayoutId,
    title: {
      ...nls.localize2("miToggleEditorLayoutWithoutMnemonic", "Flip Layout"),
      mnemonicTitle: nls.localize(
        { key: "miToggleEditorLayout", comment: ["&& denotes a mnemonic"] },
        "Flip &&Layout"
      ),
    },
  },
  order: 1,
})

registerAction2(
  class extends Action2 {
    constructor() {
      super({
        id: "workbench.action.files.saveAll",
        title: SAVE_ALL_LABEL,
        f1: true,
        icon: Codicon.saveAll,
        menu: {
          id: MenuId.ViewTitle,
          group: "navigation",
          when: ContextKeyExpr.equals("view", OpenEditorsView.ID),
          order: 20,
        },
      })
    }

    async run(accessor: ServicesAccessor): Promise<void> {
      const commandService = accessor.get(ICommandService)
      await commandService.executeCommand(SAVE_ALL_COMMAND_ID)
    }
  }
)

registerAction2(
  class extends Action2 {
    constructor() {
      super({
        id: "openEditors.closeAll",
        title: CloseAllEditorsAction.LABEL,
        f1: false,
        icon: Codicon.closeAll,
        menu: {
          id: MenuId.ViewTitle,
          group: "navigation",
          when: ContextKeyExpr.equals("view", OpenEditorsView.ID),
          order: 30,
        },
      })
    }

    async run(accessor: ServicesAccessor): Promise<void> {
      const instantiationService = accessor.get(IInstantiationService)

      const closeAll = new CloseAllEditorsAction()
      await instantiationService.invokeFunction((accessor) =>
        closeAll.run(accessor)
      )
    }
  }
)

registerAction2(
  class extends Action2 {
    constructor() {
      super({
        id: "openEditors.newUntitledFile",
        title: nls.localize2("newUntitledFile", "New Untitled Text File"),
        f1: false,
        icon: Codicon.newFile,
        menu: {
          id: MenuId.ViewTitle,
          group: "navigation",
          when: ContextKeyExpr.equals("view", OpenEditorsView.ID),
          order: 5,
        },
      })
    }

    async run(accessor: ServicesAccessor): Promise<void> {
      const commandService = accessor.get(ICommandService)
      await commandService.executeCommand(NEW_UNTITLED_FILE_COMMAND_ID)
    }
  }
)
